/**
 * Copyright (c) 2009 Anders Ekdahl (http://coffeescripter.com/)
 * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
 *
 * Version: 1.3.2
 *
 * Demo and documentation: http://coffeescripter.com/code/editable-select/
 */
(function($) {
	var $esInstances = [];

	$.fn.editableSelect = function(options) {
		var defaults = {
			bg_iframe: false,
			onSelect: false,
			items_then_scroll: 10,
			case_sensitive: false
		},
		settings = $.extend(defaults, options);

		// Only do bg_iframe for browsers that need it
		if (settings.bg_iframe && !$.browser.msie) {
			settings.bg_iframe = false;
		}

		//var instance = false;
		if (options && (typeof options).toLowerCase() == "string" && (options = $.trim(options)) != "") {
			// Extract method name and parameters
			var pos = options.indexOf('('),
				methodName = (pos < 0) ? options : $.trim(options.substr(0, pos)),
				methodParam = (pos < 0) ? null : options.substr(pos + 1).replace(/\)$/, "");

			if (typeof EditableSelect.prototype[methodName] != "undefined") {
				var eleId = "#" + $(this).attr("id") + "_hidden_select",
					idx = $(eleId).data("editable-selecter");

				if (typeof idx != "undefined" && !isNaN(idx = parseInt(idx)) && $esInstances[idx]) {
					// Call method
					if (typeof $esInstances[idx][methodName] == "function") {
						if (pos < 0 || !methodParam) return $esInstances[idx][methodName]();
						else return $esInstances[idx][methodName](methodParam);
					}
					else {
						// Return property
						return $esInstances[idx][methodName];
					}
				}

				return null;
			}
		}

		return $(this).each(function() {
			var i = $esInstances.length,
				idx = $(this).data("editable-selecter");
			if (typeof idx == "undefined" || isNaN(parseInt(idx))) {

				$esInstances[i] = new EditableSelect(this, settings);
				$(this).data("editable-selecter", i);
			}
		});
	};

	$.fn.editableSelectInstances = function() {
		var ret = [];
		$(this).each(function() {
			var idx = $(this).data("editable-selecter");

			if (typeof idx != "undefined" && !isNaN(parseInt(idx))) {
				ret[ret.length] = $esInstances[idx];
			}
		});
		return ret;
	};

	var EditableSelect = function(select, settings) {
		this.init(select, settings);
	};

	EditableSelect.prototype = {
		// --- Properties
		settings: false,
		textbox: false,
		select: false,
		select_width: 0,
		wrapper: false,
		list_item_height: 20,
		list_height: 0,
		list_is_visible: false,
		hide_on_blur_timeout: false,
		bg_iframe: false,
		current_value: "",
		// --- Methods
		value: function() {
			return this.select.val();
		},
		text: function() {
			return this.textbox.val();
		},
		init: function(select, settings) {
			this.settings = settings;

			this.wrapper = $(document.createElement("div"));
			this.wrapper.addClass("editable-select-options");

			this.select = $(select);

			this.textbox = $('<input type="text" class="' + select.className + '" autocomplete="off" />');
			this.textbox.addClass("editable-select").attr("name", this.select.attr("name"));
			this.textbox.data("editable-selecter", this.select.data("editable-selecter"));

			var id = this.select.attr("id");
			if (!id) id = "editable-select" + $esInstances.length;

			this.textbox.attr("id", id);
			this.select.attr("id", id + "_hidden_select");

			this.select.after(this.textbox);

			if (this.select.css("display") == "none") {
				this.textbox.css("display", "none");
			}
			else if (this.select.css("visibility") == "hidden") {
				this.textbox.css("visibility", "visible");
			}

			// Set to hidden, because we want to call .show() on it
			// to get it's width but not having it display on the screen
			this.select.css("visibility", "hidden").hide();

			this.initInputEvents(this.textbox);

			var _self = this;
			setTimeout(function() {
				_self.duplicateOptions();
				_self.setWidths(false);

				// Because we don't want the value of the select when the form is submitted
				_self.select.attr("disabled", "disabled");
			}, 10);

			this.wrapper.appendTo($("body"));

			if (this.settings.bg_iframe) {
				this.createBackgroundIframe();
			}
		},
		/**
		 * Take the select lists options
		 * and populate an unordered list with them.
		 */
		duplicateOptions: function() {
			var _self = this,
				option_list = $(document.createElement("ul")).appendTo(this.wrapper);

			// DEBUG
			//if (typeof console != "undefined")
			//	console.log("duplicateOptions(), select[#" + this.select.attr("id") + "].selectedIndex = " + this.select.get(0).selectedIndex);
			var el = this.select.get(0),
				selectedIdx = (typeof el.selectedIndex != "undefined" && el.selectedIndex >= 0) ? el.selectedIndex : 0;

			this.select.find("option").each(function(idx) {
				if (selectedIdx === idx) {// $(this).is(":selected")
					_self.current_value = $(this).text();//$(this).val();
					_self.textbox.val(_self.current_value);
				}

				var li = $("<li>" + $(this).html() + "</li>");//.attr("title", $(this).val());
				_self.initListItemEvents(li);

				option_list.append(li);
			});

			this.checkScroll();
		},
		/**
		 * Re-bind combobox data-list.
		 */
		refreshOptions: function(keepSelectedIndex) {
			// TODO: unbind all list-item events (may not needle)
			this.wrapper.find("ul li").each(function() {
				$(this).unbind("mouseover").unbind("mousedown");
			});

			this.wrapper.empty();

			// Reset selected index of the rebinded dropdownbox
			var el = this.select.get(0);
			if (typeof el.selectedIndex != "undefined"
				&& (typeof keepSelectedIndex == "undefined" || !keepSelectedIndex)) {
				el.selectedIndex = 0;
			}

			this.duplicateOptions();
			this.setWidths(false);
		},
		refreshState: function() {
			var el = this.select.get(0);
			// DEBUG
			//if (typeof console != "undefined") console.log("EditableSelect.refreshState(), select.selectedIndex = " + el.selectedIndex);

			if (typeof el.selectedIndex != "undefined" && el.selectedIndex >= 0) {
				var els = this.select.find("option:eq(" + el.selectedIndex + ")");
				if (els.length) {
					this.current_value = els.text();
					this.textbox.val(this.current_value);
				}
			}
		},
		/**
		 * Check if the list has enough items to display a scroll.
		 */
		checkScroll: function() {
			if (this.wrapper.find("ul li").length > this.settings.items_then_scroll) {
				this.list_height = this.list_item_height * this.settings.items_then_scroll;

				this.wrapper.height(this.list_height).css("overflow", "auto");
			}
			else {
				this.wrapper.height("auto").css("overflow", "visible");
			}
		},
		addOption: function(text, value) {
			var opt = $("<option>" + text + "</option>"),
				li = $("<li>" + text + "</li>");

			if (typeof value != "undefined") {
				opt.val(value);
				//li.attr("title", value);
			}

			this.select.append(opt);

			this.initListItemEvents(li);
			this.wrapper.find("ul").append(li);

			this.setWidths(false);
			this.checkScroll();
		},
		hideDropdownList: function(dontBlur) {
			if (typeof dontBlur == "undefined" || !dontBlur)
				this.textbox.blur();

			if (this.listIsVisible()) {
				this.clearSelectedListItem();
				this.hideList();
			}
		},
		/**
		 * Init the different events on the input element.
		 */
		initInputEvents: function(text) {
			var _self = this,
				timer = false;

			// Hide dropdown box on blur
			$(document).mousedown(function(evt) {
				if (evt && evt.target) {
					var jObj = $(evt.target);

					if (jObj && (jObj.hasClass("editable-select-options")
						|| jObj.parents(".editable-select-options").length || jObj.is("input.editable-select:text"))) {
						return;
					}
				}

				//_self.pickListItem();
				_self.hideDropdownList();
			});

			$(window).scroll(function() { _self.hideDropdownList(); })
				.resize(function() { _self.hideDropdownList(); });

			// Hide the list when the owner input is hidden
			text.parents(".popup-wrapper:first, .fancybox-wrap:first").bind("hide", function() {
				$(window).focus();
				_self.hideDropdownList();
			});

			text.focus(function() {
				$(this).select();

				// Can't use the blur event to hide the list, because the blur event
				// is fired in some browsers when you scroll the list
				if ((typeof this.isClicking == "undefined" || !this.isClicking)
					&& !_self.listIsVisible() && !$(this).parents(":hidden").length) {

					_self.showList();
					_self.highlightSelected();
				}

			}).blur(function() {
				// Restrict editable value with the combobox values
				if (!$(this).hasClass("editable-combobox")
					&& (typeof this.isClicking == "undefined" || !this.isClicking)) {

					if (!_self.listIsVisible()) {
						_self.highlightSelected();
						_self.pickListItem();
					}
					else {
						setTimeout(function() {
							if (!_self.listIsVisible()) {
								_self.highlightSelected();
								_self.pickListItem();
							}
						}, 0);
					}
				}

			}).mousedown(function(e) {
				// Left mouse button was pressed, set flag
				if (e && e.which === 1) this.isClicking = true;

			}).mouseup(function(e) {
				if (e && e.which === 1) {
					//e.stopPropagation();
					if (_self.listIsVisible()) _self.hideDropdownList(true);
					else {
						_self.showList();
						_self.highlightSelected();
					}
					this.isClicking = false;
				}

			}).keydown(function(e) {	// Capture key events so the user can navigate through the list
				switch (e.keyCode) {
					// Down
					case 40:
						if (!_self.listIsVisible()) {
							_self.showList();
							_self.highlightSelected();
						}
						else {
							e.preventDefault();
							_self.selectNewListItem("down");
							return false;
						}
						break;
					// Up
					case 38:
						e.preventDefault();
						_self.selectNewListItem("up");
						return false;
					// Tab
					case 9:
						_self.pickListItem();
						break;
					// Esc
					case 27:
						e.preventDefault();
						_self.hideDropdownList(true);
						return false;
					// Enter, prevent form submission
					case 13:
						e.preventDefault();
						_self.pickListItem();
						return false;
				}

			}).keyup(function(e) {
				// Prevent lots of calls if it's a fast typer
				if (timer) {
					clearTimeout(timer);
					timer = false;
				}

				timer = setTimeout(function() {
					// If the user types in a value, select it if it's in the list
					if (_self.textbox.val() != _self.current_value) {
						_self.current_value = _self.textbox.val();
						_self.highlightSelected();
					}
				}, 200);

			}).keypress(function(e) {
				if (e.keyCode == 13) {
					// Enter, prevent form submission
					e.preventDefault();
					return false;
				}
			});

		},
		initListItemEvents: function(list_item) {
			var _self = this;

			list_item.mouseover(function() {
				//_self.clearSelectedListItem();
				_self.selectListItem(list_item);

			}).mousedown(function(e) {
				if (e && e.which === 1) {
					// Needs to be mousedown and not click, since the inputs blur events
					// fires before the list items click event
					e.stopPropagation();
					_self.pickListItem($(this));

					return false;
				}
			});
		},
		selectNewListItem: function(direction) {
			var li = this.selectedListItem();
			if (!li.length) {
				li = this.selectFirstListItem();
			}

			var sib = (direction == "down") ? li.next() : li.prev();
			if (sib.length) {
				this.selectListItem(sib);
				this.scrollToListItem(sib);
				this.unselectListItem(li);
			}
		},
		selectListItem: function(list_item) {
			this.clearSelectedListItem();
			list_item.addClass("selected");
		},
		selectFirstListItem: function() {
			this.clearSelectedListItem();
			return this.wrapper.find("li:first").addClass("selected");
		},
		unselectListItem: function(list_item) {
			list_item.removeClass("selected");
		},
		selectedListItem: function() {
			return this.wrapper.find("li.selected:first");
		},
		selectedListItems: function() {
			return this.wrapper.find("li.selected");
		},
		clearSelectedListItem: function() {
			this.wrapper.find("li.selected").removeClass("selected");
		},
		/**
		 * The difference between this method and selectListItem()
		 * is that this method also changes the text field and then hides the list
		 */
		pickListItem: function(list_item) {
			var does_exists = false,
				_self = this;

			if (typeof list_item != "undefined" && list_item && list_item.length) {

				this.wrapper.find("ul li").each(function(idx, elx) {
					if (_self.textbox.val() == $(elx).text()) {
						does_exists = true;
						return false;	// break the $.each() loop
					}
				});
			}

			if (!does_exists) {
				list_item = this.selectedListItem();
			}

			if (list_item && list_item.length) {

				this.current_value = list_item.text();
				this.textbox.val(this.current_value);

				this.select.find("option").each(function() {
					if (/*(typeof list_item.attr("title") != "undefined" && $(this).val() == list_item.attr("title"))
						|| */$(this).text() == _self.current_value) {	// -or- $(this).html() == _self.current_value

						_self.select.val($(this).val());
						return false;	// break the loop
					}
				});

				if (typeof this.settings.onSelect == "function") {
					this.settings.onSelect.call(this, list_item);
				}

				this.hideList();
			}
		},
		listIsVisible: function() {
			return this.list_is_visible;
		},
		showList: function() {
			if (!this.list_is_visible) {
				this.positionElements();
				this.setWidths(true);

				this.wrapper.show();

				this.hideOtherLists();
				this.list_is_visible = true;

				if (this.settings.bg_iframe && this.bg_iframe) {
					this.showBackgroundIframe();
				}
			}
		},
		highlightSelected: function() {
			var _self = this,
				current_value = this.textbox.val();

			if (!current_value) {
				/*if (highlight_first)*/this.selectFirstListItem();
				return;
			}

			if (!this.settings.case_sensitive) {
				current_value = current_value.toLowerCase();
			}

			var best_candiate = false,
				value_found = false;

			this.wrapper.find("ul li").each(function(idx) {
				var el = $(this),
					text = el.text();
				if (!_self.settings.case_sensitive) {
					text = text.toLowerCase();
				}

				if (text == current_value) {
					value_found = true;

					//_self.clearSelectedListItem();
					_self.selectListItem(el);
					_self.scrollToListItem(el);

					return false;
				}
				else if (!best_candiate && text.indexOf(current_value) >= 0) {
					// Can't do return false here, since we still need to iterate over
					// all list items to see if there is an exact match
					best_candiate = el;
				}
			});

			if (!value_found) {
				if (best_candiate) {
					//this.clearSelectedListItem();
					this.selectListItem(best_candiate);
					this.scrollToListItem(best_candiate);
				}
				else {
					this.selectFirstListItem();
				}
			}
		},
		scrollToListItem: function(list_item) {
			if (this.list_height > 0 && this.listIsVisible()) {
				// DEBUG
				/*if (typeof console != "undefined") {
					console.log("scrollToListItem(), wrapper.offsetTop = " + this.wrapper.offset().top
						+ ", li.offsetTop = " + list_item.offset().top + ", li.offsetTop = " + list_item.get(0).offsetTop + ", li.text = " + list_item.text());
				}*/
				var offSet = list_item.get(0).offsetTop;
				if (!offSet || offSet <= 0) offSet = list_item.offset().top - this.wrapper.offset().top;
				//
				offSet = Math.max(0, offSet - this.wrapper.outerHeight() / 2);
				this.wrapper.scrollTop(offSet);
			}
		},
		hideList: function() {
			this.wrapper.hide();
			this.list_is_visible = false;

			if (this.settings.bg_iframe && this.bg_iframe) {
				this.bg_iframe.hide();
			}
		},
		hideOtherLists: function() {
			var idx = this.select.data("editable-selecter");
			if (typeof idx != "undefined") idx = parseInt(idx);

			for (var i = 0; i < $esInstances.length; i++) {
				if (i !== idx) $esInstances[i].hideDropdownList();
			}
		},
		positionElements: function() {
			var ofs = this.textbox.offset(),
				iHeight = this.textbox.outerHeight();

			this.wrapper.css("left", ofs.left + "px");

			// Need to do this in order to get the list item height
			this.wrapper.css("visibility", "hidden").show();

			if ((ofs.top + iHeight + this.wrapper.outerHeight()) > ($(window).scrollTop() + $(window).height())) {
				ofs.top -= this.wrapper.outerHeight() + 1;	// TODO: +1 ???
			}
			else ofs.top += iHeight;
			this.wrapper.css("top", ofs.top + "px");

			this.list_item_height = this.wrapper.find("li:first").outerHeight();

			this.wrapper.css("visibility", "visible").hide();
		},
		setWidths: function(initialized) {
			if (!initialized) {
				// The text input has a right margin because of the background arrow image
				// so we need to remove that from the width
				this.select.show();
				var newWidth = this.select.outerWidth();
				this.select.hide();

				var padd = this.textbox.outerWidth() - this.textbox.width();
				this.textbox.width(newWidth - padd);

				var iWidth = this.textbox.outerWidth();
				this.wrapper.width(iWidth - 2);
			}
			else {
				this.wrapper.css("visibility", "hidden").show();

				// Fix scroll-width for the dropdown box
				var newWidth = this.wrapper.get(0).scrollWidth;
				if (this.wrapper.outerWidth() < newWidth) {

					var padd = 0;
					if (this.wrapper.find("ul li").length > this.settings.items_then_scroll) {
						padd = (typeof getScrollbarWidth != "undefined") ? getScrollbarWidth() : 17;
					}
					this.wrapper.width(newWidth + padd + 1);
				}

				// Fix offset left
				var iWidth = this.textbox.outerWidth();
				if (this.wrapper.outerWidth() > iWidth
					&& (this.wrapper.offset().left + this.wrapper.outerWidth()) > ($(window).scrollLeft() + $(window).width())) {

					this.wrapper.css("left", (this.textbox.offset().left + iWidth - this.wrapper.outerWidth()) + "px");// TODO: -1 ???
				}

				this.wrapper.css("visibility", "visible").hide();
			}
		},
		createBackgroundIframe: function() {
			var bg_iframe = $('<iframe frameborder="0" class="editable-select-iframe" src="about:blank;"></iframe>')
				.appendTo($("body"));

			bg_iframe.width(this.select.outerWidth()).height(this.wrapper.outerHeight());

			this.bg_iframe = bg_iframe;
		},
		// Background-IFrame always behind the dropdown box
		showBackgroundIframe: function() {
			this.bg_iframe
				.width(this.wrapper.outerWidth()).height(this.wrapper.outerHeight())
				.css({ top: this.wrapper.css("top"), left: this.wrapper.css("left") })
				.show();
		}
	};
})(jQuery);
